﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Charisma.Conventions;

namespace Charisma
{
    public class DbModelBuilder
    {
        private Type contextType;
        private DbModel model;

        public DbModelBuilder(Type contextType)
        {
            this.contextType = contextType;
            model = new DbModel(contextType);
            Conventions = new ConventionSet();
            Configurations = new ConfigurationRegistry();
        }

        public ConventionSet Conventions { get; private set; }

        public ConfigurationRegistry Configurations { get; private set; }

        public RepositoryConfigurator<T> Repository<TContext,T>(Expression<Func<TContext, IRepository<T>>> expression)
            where T : class
        {
            Contract.Requires(expression != null);
            Contract.Requires<NotSupportedException>(expression.Body is MemberExpression,
                "Expressions other then 'MemberExpression' are not supported yet.");
            Contract.Requires<NotSupportedException>(
                (((MemberExpression) expression.Body).Member as PropertyInfo) != null, "Only properties are supported.");

            var me = (MemberExpression) expression.Body;
            var result = new RepositoryConfigurator<T>(me.Member);
            Configurations.Add(result);
            return result;
        }

        public EntityConfigurator<T> Entity<T>() where T : class
        {
//            Type type = typeof (T);
//            EntityConfiguration configuration = FindTypeConfiguration(type);
//            if (configuration == null)
//                configuration = RegisterType(type);
            var result = new EntityConfigurator<T>();
            Configurations.Add(result);
            return result;
        }

        internal DbModel Build()
        {
            DiscoverRepositories();
            DiscoverRoots();
            return model;
        }

        private void DiscoverRepositories()
        {
            var candidates = Conventions.Get<IRepositoryConvention>()
                .SelectMany(c => c.Find(contextType))
                .Distinct();

            foreach (var candidate in candidates) {
                var genericImplementorType = ReflectionService.GetGenericImplementorType(candidate.PropertyType);

                // Getting ElementType
                var elementType = genericImplementorType.GetGenericArguments().First();

                // Registering root & repository
                var root = RegisterType(elementType);
                RegisterRepository(candidate, root);
            }
        }

        private void DiscoverRoots()
        {
            foreach (var repository in model.Repositories) {
                var rootElement = repository.RootElement;

                var members = Conventions.Get<IMemberConvention>()
                    .SelectMany(c => c.Find(rootElement.Type))
                    .Distinct();
                foreach (var member in members)
                    rootElement.RegisterMember(member);

                var keyMember = Conventions.Get<IKeyMemberConvention>()
                    .Select(c => c.Find(rootElement.Type, members))
                    .FirstOrDefault();
                if (keyMember != null)
                    rootElement.RegisterKeyMember(keyMember);
            }
        }

        private void DiscoverFields(EntityConfiguration configuration)
        {
            var parent = FindParentConfiguration(configuration);

            if (parent != null)
                foreach (var member in parent.Members)
                    configuration.Members.Add(member);

            var members = Conventions.Get<IMemberConvention>()
                .SelectMany(c => c.Find(configuration.Type))
                .Distinct();
            foreach (var member in members)
                configuration.RegisterMember(member);

//            IEnumerable<MemberPair> memberPairs = Conventions.ReferenceMemberConvention.Find(configuration.Type, candidates);
//            foreach (MemberPair memberPair in memberPairs) {
//                MemberPair item = memberPair;
//
//                // Trying to find whether any repository root is assignable from the reference type.
//                EntityConfiguration root = model.Repositories
//                    .Where(r => r.RootElement.Type.IsAssignableFrom(item.PrimaryMember.GetMemberType()))
//                    .Select(r => r.RootElement)
//                    .SingleOrDefault();
//                if (root == null)
//                    continue;
//
//                EntityMemberConfiguration primaryMember = configuration.Members.Single(p => p.Field == item.PrimaryMember);
//                EntityMemberConfiguration backingMember = configuration.Members.Single(p => p.Field == item.BackingMember);
//
//                primaryMember.BackingMember = backingMember;
//                primaryMember.IsIgnored = true;
//                backingMember.PrimaryMember = primaryMember;
//            }
        }

        internal EntityConfiguration FindTypeConfiguration(Type type)
        {
            var result = model.Entities.Find(type);
            if (result != null)
                return result;

            if (!model.Repositories.Any(repository => repository.RootElement.Type.IsAssignableFrom(type)))
                return null;

            result = RegisterType(type);
            DiscoverFields(result);
            return result;
        }

        internal EntityConfiguration FindParentConfiguration(EntityConfiguration child)
        {
            var parentType = child.Type.BaseType;
            while (parentType != typeof(object)) {
                var result = FindTypeConfiguration(parentType);
                if (result != null)
                    return result;
                parentType = parentType.BaseType;
            }
            return null;
        }

        #region Repository building members

        internal RepositoryConfiguration RegisterRepository(PropertyInfo property, EntityConfiguration root)
        {
            Contract.Requires(property != null);
            Contract.Requires(root != null);

            var result = model.Repositories.Find(property);
            if (result != null)
                return result;

            result = BuildRepository(property, root);

            if (model.Repositories.Any(r => r.Name == result.Name))
                throw new InvalidOperationException(String.Format("Repository with name '{0}' is already registered", result.Name));

            model.Repositories.Add(result);
            return result;
        }

        private static RepositoryConfiguration BuildRepository(PropertyInfo property, EntityConfiguration root)
        {
            Contract.Requires(property != null);
            Contract.Requires(root != null);

            return new RepositoryConfiguration(property, root);
        }

        #endregion

        #region Type building members

        internal EntityConfiguration RegisterType(Type type)
        {
            Contract.Requires(type != null);

            var result = model.Entities.Find(type);
            if (result != null)
                return result;

            result = BuildEntity(type);
            model.Entities.Add(result);
            return result;
        }

        private static EntityConfiguration BuildEntity(Type type)
        {
            Contract.Requires(type != null);

            var result = new EntityConfiguration(type);
            return result;
        }

        #endregion
    }
}
